//-------------------------------------------------------------------------------------------------
// Copyright (c) Bradford W. Mott and Flare Contributors
// North Carolina State University, Department of Computer Science
// The IntelliMedia Group
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//-------------------------------------------------------------------------------------------------

using UnityEngine;
using System.Collections.Generic;

using Flare.Geom;
using Flare.Script;
using Flare.Media;

namespace Flare.Display
{
    /// <summary>
    /// The MovieClip class is a display object that includes a timeline. Movie clips
    /// provide the main animation support in Flare and are authored with Flash.
    /// </summary>
    public class MovieClip : Sprite
    {
        /// <summary>
        /// Frame number of the playhead's location in the current scene (first frame is numbered 1)
        /// </summary>
        public int currentFrame { get; private set; }

        /// <summary>
        /// Frame label of the playhead's location in the current scene if there is
        /// one; otherwise, null.
        /// </summary>
        public string currentFrameLabel
        {
            get { return (this.currentFrame > 0) ? frameLabels[this.frameIndex] : null; }
        }

        /// <summary>
        /// Frame label of the playhead's location in the current scene. If the
        /// playhead's location has no label then the label of the previous frame
        /// that included a label is used; otherwise, null.
        /// </summary>
        public string currentLabel
        {
            get { return (this.currentFrame > 0) ? labels[this.frameIndex] : null; }
        }

        /// <summary>
        /// Array of all the frame labels for the current scene.
        /// </summary>
        public FrameLabel[] currentLabels
        {
            get { return scenes[currentSceneIndex].labels; }
        }

        /// <summary>
        /// Scene the playhead is currently located in.
        /// </summary>
        public Scene currentScene
        {
            get { return scenes[currentSceneIndex]; }
        }

        /// <summary>
        /// Array of all the MovieClip's scenes.
        /// </summary>
        public Scene[] scenes { get; internal set; }

        /// <summary>
        /// Total number of frames in the MovieClip. This counts the frames in all of
        /// the MovieClip's scenes.
        /// </summary>
        public int totalFrames { get; private set; }

        /// <summary>
        /// Indicates whether the MovieClip is playing or not. Unlike AS3 this property always
        /// reflects the current state of the MovieClip even if navigation methods have not
        /// been called (e.g., GotoFrame, etc.).
        /// </summary>
        public bool isPlaying { get; private set; }

        /// <summary>
        /// Indicates whether this is the main timeline or not.
        /// </summary>
        internal bool mainTimeline { get; set; }

        /// <summary>
        /// Represents the MovieClip's timeline.
        /// </summary>
        internal Timeline timeline { get; set; }
  
        internal SoundStream streamSound { get; set; }
        
        /// <summary>
        /// Index within the scenes array indicating which one is the currentScene.
        /// </summary>
        private int currentSceneIndex { get; set; }

        /// <summary>
        /// Starting frame for the current scene (zero-based). This must be updated whenever
        /// currentSceneIndex changes values.
        /// </summary>
        private int currentSceneStartingFrame { get; set; }

        /// <summary>
        /// Sorted map of display list depths to corresponding display objects at that depth.
        /// This is used internally by the MovieClip to handle executing timeline actions
        /// for adding, removing, and updating display objects on the display list.
        /// </summary>
        private SortedList<int, DisplayObject> depthObjectMap { get; set; }

        /// <summary>
        /// Array used for the currentFrameLabel property. Initialized once the MovieClip
        /// finished loading.
        /// </summary>
        private string[] frameLabels { get; set; }

        /// <summary>
        /// Array used for the currentLabel property. Initialized once the MovieClip
        /// finished loading.
        /// </summary>
        private string[] labels { get; set; }

        /// <summary>
        /// Returns the zero-based frame index of the current frame in the current scene.
        /// </summary>
        private int frameIndex
        {
            get { return this.currentSceneStartingFrame + this.currentFrame - 1; }
        }

        /// <summary>
        /// Create a new movie clip object.
        /// </summary>
        public MovieClip()
            : this(false, 1)
        {
        }

        internal MovieClip(bool mainTimeline, int totalFrames)
        {
            this.currentFrame = 0;
            this.scenes = new Scene[1] { new Scene("", totalFrames, new FrameLabel[0]) };
            this.totalFrames = totalFrames;
            this.isPlaying = true;

            this.mainTimeline = mainTimeline;
            this.timeline = new Timeline(totalFrames);

            this.currentSceneIndex = 0;
            this.currentSceneStartingFrame = 0;
            this.depthObjectMap = new SortedList<int, DisplayObject>();
            this.frameLabels = new string[totalFrames];
            this.labels = new string[totalFrames];
        }

        internal void PostLoadInitialization()
        {
            // Construct timeline data based on all of the display tags
            this.timeline.BuildTimeLineDisplayEntries();

            // Create the frame label data based on the scene information that was loaded.
            // This is used for the currentFrameLabel and currentLabel properties.
            foreach (var scene in this.scenes)
            {
                foreach (var label in scene.labels)
                {
                    this.frameLabels[label.frame - 1] = label.name;
                }
            }

            int frameOffset = 0;
            foreach (var scene in this.scenes)
            {
                string lastLabel = null;
                for (int i = frameOffset; i < (frameOffset + scene.numFrames); ++i)
                {
                    if (this.frameLabels[i] != null)
                    {
                        lastLabel = this.frameLabels[i];
                    }
    
                    this.labels[i] = lastLabel;
                }
                frameOffset += scene.numFrames;
            }
        }

        /// <summary>
        /// Return a clone of the MovieClip with a deep copy of any mutable members.
        /// </summary>
        public override object Clone()
        {
            MovieClip clone = (MovieClip)base.Clone();

            // Use the shallow copied currentFrame, scenes, totalFrames, isPlaying, mainTimeline,
            // timeline, currentSceneIndex, currentSceneStartingFrame, frameLabels, and labels.

            // NOTE: We're using shallow copies of scenes, timeline, frameLabels, and labels
            // since they are immutable

            // TODO bwmott 2013-06-23: Might consider supporting cloning in this case; however,
            // it's probably not worth it.
            if (this.depthObjectMap.Count > 0)
            {
                throw new global::System.InvalidOperationException(
                    "Cannot clone a MovieClip that's being displayed.");
            }

            clone.depthObjectMap = new SortedList<int, DisplayObject>();

            return clone;
        }

        /// <summary>
        /// Play the SWF file starting at the specified frame.
        /// </summary>
        public void GotoAndPlay(int frame, string sceneName = null)
        {
            this.GotoFrame(frame, sceneName);
            this.Play();
        }

        /// <summary>
        /// Play the SWF file starting at the named frame.
        /// </summary>
        public void GotoAndPlay(string frame, string sceneName = null)
        {
            this.GotoFrame(frame, sceneName);
            this.Play();
        }

        /// <summary>
        /// Move the playhead to the specified frame and stop playing.
        /// </summary>
        public void GotoAndStop(int frame, string sceneName = null)
        {
            this.GotoFrame(frame, sceneName);
            this.Stop();
        }

        /// <summary>
        /// Move the playhead to the named frame and stop playing.
        /// </summary>
        public void GotoAndStop(string frame, string sceneName = null)
        {
            this.GotoFrame(frame, sceneName);
            this.Stop();
        }

        private void GotoFrame(string label, string sceneName)
        {
            int si = (sceneName == null) ? this.currentSceneIndex : this.GetSceneIndex(sceneName);
            if (si < 0)
            {
                throw new global::System.ArgumentException("specified scene does not exist.");
            }

            int frame = -1;
            foreach (var fl in this.scenes[si].labels)
            {
                if (fl.name.Equals(label))
                {
                    frame = fl.frame;
                    break;
                }
            }

            if (frame < 0)
            {
                throw new global::System.ArgumentException("specified frame label does not exist.");
            }
            else
            {
                this.GotoFrame(frame, sceneName);
            }
        }

        private void GotoFrame(int frame, string sceneName)
        {
            if ((sceneName == null) || (this.currentSceneIndex == this.GetSceneIndex(sceneName)))
            {
                if ((frame <= 0) || (frame > this.currentScene.numFrames))
                {
                    throw new global::System.ArgumentException("specified frame out of range.");
                }

                NavigateToFrame(this.currentSceneStartingFrame + this.currentFrame,
                    this.currentSceneStartingFrame + frame);

                if (this.currentFrame != frame)
                {
                    this.currentFrame = frame;
                    QueueAS2ActionsAtFrame(this.frameIndex);
                    ApplySoundActions(this.frameIndex);
                }
            }
            else
            {
                int si = this.GetSceneIndex(sceneName);
                if (si < 0)
                {
                    throw new global::System.ArgumentException("specified scene does not exist.");
                }

                if ((frame <= 0) || (frame > this.scenes[si].numFrames))
                {
                    throw new global::System.ArgumentException("specified frame out of range.");
                }

                this.GotoScene(si, frame);
            }
        }

        /// <summary>
        /// Goto the specified scene and frame.
        /// </summary>
        private void GotoScene(int sceneIndex, int frame)
        {
            int currentFrameIndex = this.currentSceneStartingFrame + this.currentFrame;

            this.currentSceneIndex = sceneIndex;
            this.currentSceneStartingFrame = 0;
            for (int i = 0; i < this.currentSceneIndex; ++i)
            {
                this.currentSceneStartingFrame += this.scenes[i].numFrames;
            }

            NavigateToFrame(currentFrameIndex, this.currentSceneStartingFrame + frame);

            this.currentFrame = frame;
            QueueAS2ActionsAtFrame(this.frameIndex);
        }

        /// <summary>
        /// Start playing the movie at the current playhead position.
        /// </summary>
        public void Play()
        {
            this.isPlaying = true;
        }

        /// <summary>
        /// Stop playing the movie at the current playhead position.
        /// </summary>
        public void Stop()
        {
            this.isPlaying = false;
        }

        /// <summary>
        /// Move the playhead to the next frame and stop playing.
        /// </summary>
        public void NextFrame()
        {
            this.AdvanceFrame();
            this.Stop();
        }

        /// <summary>
        /// Move the playhead to the previous frame and stop playing.
        /// </summary>
        public void PrevFrame()
        {
            // Are we moving past the beginning of the current scene?
            if ((this.currentFrame - 1) < 1)
            {
                // Is there another scene to move to?
                if (this.scenes.Length > 1)
                {
                    int si = (this.scenes.Length + this.currentSceneIndex - 1) % scenes.Length;
                    this.GotoScene(si, this.scenes[si].numFrames);
                }
                // Only one scene, so let's wrap around to the end of the scene
                else
                {
                    NavigateToFrame(this.currentSceneStartingFrame + this.currentFrame,
                        this.currentSceneStartingFrame + this.currentScene.numFrames);

                    this.currentFrame = this.currentScene.numFrames;
                    QueueAS2ActionsAtFrame(this.frameIndex);
                }
            }
            // Haven't reached the beginning of the current scene so move back to the prior frame
            else
            {
                NavigateToFrame(this.currentSceneStartingFrame + this.currentFrame,
                    this.currentSceneStartingFrame + this.currentFrame - 1);

                this.currentFrame -= 1;
                QueueAS2ActionsAtFrame(this.frameIndex);
            }

            this.Stop();
        }

        /// <summary>
        /// Move the playhead to the beginning of the next scene.
        /// </summary>
        public void NextScene()
        {
            int si = (this.currentSceneIndex + 1) % this.scenes.Length;
            if (si != this.currentSceneIndex)
            {
                this.GotoScene(si, 1);
            }
        }

        /// <summary>
        /// Move the playhead to the beginning of the previous scene.
        /// </summary>
        public void PrevScene()
        {
            int si = (this.scenes.Length + this.currentSceneIndex - 1) % scenes.Length;
            if (si != this.currentSceneIndex)
            {
                this.GotoScene(si, 1);
            }
        }

        private int GetSceneIndex(string name)
        {
            for (int i = 0; i < this.scenes.Length; ++i)
            {
                if (this.scenes[i].name.Equals(name))
                {
                    return i;
                }
            }

            return -1;
        }

        // Gets any possible script in the current frame.
        public string[] GetScriptActions(int currentFrame)
        {
            var actions = timeline.GetActionsAt(currentFrame);
            
            for (int a = 0; a < actions.Count; a++)
            {
                var action = actions [a];
                
                if (action is AS2Action)
                {
                    AS2 as2 = ((AS2Action)action).actions;
                    
                    return as2.GetScriptContents();
                }
            }  
            return null;
        }

        public override DisplayObject RemoveChild(DisplayObject child)
        {
            DisplayObject removed = base.RemoveChild(child);

            // Since scripts might remove display objects added via the timeline we must
            // check to see if we need to remove an entry from the depthObjectMap
            int depthIndex = this.depthObjectMap.IndexOfValue(removed);
            if (depthIndex >= 0)
            {
                this.depthObjectMap.RemoveAt(depthIndex);
            }

            return removed;
        }

        public override DisplayObject RemoveChildAt(int index)
        {
            DisplayObject removed = base.RemoveChildAt(index);

            // Since scripts might remove display objects added via the timeline we must
            // check to see if we need to remove an entry from the depthObjectMap
            int depthIndex = this.depthObjectMap.IndexOfValue(removed);
            if (depthIndex >= 0)
            {
                this.depthObjectMap.RemoveAt(depthIndex);
            }

            return removed;
        }

        public override void RemoveChildren()
        {
            base.RemoveChildren();

            // All children are being removed so clear the depthObjectMap as well
            this.depthObjectMap.Clear();
        }

        internal void Update()
        {
            if (this.isPlaying)
            {
                AdvanceFrame();
            }
        }

        private void AdvanceFrame()
        {
            // Have we reached the end of the current scene?
            if ((this.currentFrame + 1) > this.currentScene.numFrames)
            {
                // Is there another scene to move to?
                if (this.scenes.Length > 1)
                {
                    NextScene();
                }
                // Only one scene, so let's wrap around to the beginning of the timeline or pause
                else
                {
                    if (!this.mainTimeline)
                    {
                        if (this.currentFrame != 1)
                        {
                            NavigateToFrame(this.currentSceneStartingFrame + this.currentFrame, 1);

                            this.currentFrame = 1;
                            QueueAS2ActionsAtFrame(this.frameIndex);
                        }
                    }
                    else
                    {
                        // Main timeline automatically stops playing when it reaches the end.
                        Stop();
                    }
                }
            }
            // Haven't reached the end of the current scene so advance to the next frame
            else
            {
                NavigateToFrame(this.currentSceneStartingFrame + this.currentFrame,
                    this.currentSceneStartingFrame + this.currentFrame + 1);

                this.currentFrame += 1;
                ApplySoundActions(this.frameIndex);
                QueueAS2ActionsAtFrame(this.frameIndex);
                ApplySoundStreamActions(this.frameIndex);
            }
        }

        private static Dictionary<int, DisplayEntry> ms_emptyDepthDictionary =
            new Dictionary<int, DisplayEntry>();

        private void NavigateToFrame(int oldFrame, int newFrame)
        {
            int oldFrameIndex = oldFrame - 1;
            int newFrameIndex = newFrame - 1;

            Dictionary<int, DisplayEntry> oldFrameEntries;
            Dictionary<int, DisplayEntry> newFrameEntries;

            // Exit if already at the specified frame
            if (oldFrameIndex == newFrameIndex)
            {
                return;
            }
            // Is this the first frame that's being displayed?
            else if (oldFrameIndex == -1)
            {
                oldFrameEntries = ms_emptyDepthDictionary;
                newFrameEntries = this.timeline.timelineDisplayEntries[newFrameIndex];
            }
            else
            {
                oldFrameEntries = this.timeline.timelineDisplayEntries[oldFrameIndex];
                newFrameEntries = this.timeline.timelineDisplayEntries[newFrameIndex];
            }

            // Remove objects from the display list at depths not utilized in the new frame
            foreach (var oldEntry in oldFrameEntries)
            {
                int depth = oldEntry.Key;
                if (!newFrameEntries.ContainsKey(depth))
                {
                    this.RemoveChild(this.depthObjectMap[depth]);
                }
            }

            // Add and/or update objects on the display list in the new frame
            foreach (var timelineEntry in newFrameEntries)
            {
                int depth = timelineEntry.Key;
                DisplayEntry entry = timelineEntry.Value;
                DisplayEntry current = null;

                if (!oldFrameEntries.TryGetValue(depth, out current))
                {
                    // Add new object at the specified depth
                    object definition;
                    if (this.loaderInfo.context.dictionary.TryGetValue(
                        entry.characterId, out definition))
                    {
                        DisplayObject newObj;
                        if (definition is Sprite)
                        {
                            ((Sprite)definition).graphics.CacheMeshes();
                            newObj = (DisplayObject)((DisplayObject)definition).Clone();
                        }
                        else if (definition is DisplayObject)
                        {
                            newObj = (DisplayObject)((DisplayObject)definition).Clone();
                        }
                        else
                        {
                            // For SWFs that place erroneous characters on the display list
                            // (e.g., fonts), create an empty Shape instead
                            newObj = new Shape();
                        }

                        this.depthObjectMap.Add(depth, newObj);
                        int addedAt = this.depthObjectMap.IndexOfKey(depth);
                        if (addedAt == (this.depthObjectMap.Count - 1))
                        {
                            // New object is higher that anything else so just add it
                            this.AddChild(newObj);
                        }
                        else
                        {
                            // Find the object that's right above newObj in the depth map
                            DisplayObject above = this.depthObjectMap.Values[addedAt + 1];

                            // Insert newObj at the location of that object
                            this.AddChildAt(newObj, this.GetChildIndex(above));
                        }
                    }
                    else
                    {
                        Log.Error(Subsystem.Playback,
                            "Unknown Character Id: {0}", entry.characterId);
                    }
                }
                else
                {
                    // If it's a different character then remove current object and add new one.
                    // Otherwise, it's the same character so see if they were added at different
                    // times, if so we need to remove and readd the object to the timeline
                    if ((entry.characterId != current.characterId) ||
                        (entry.characterIdFrame != current.characterIdFrame))
                    {
                        // Remove the existing object at the specified depth
                        this.RemoveChild(this.depthObjectMap[depth]);

                        // Add new object at the specified depth
                        object definition;
                        if (this.loaderInfo.context.dictionary.TryGetValue(
                            entry.characterId, out definition))
                        {
                            DisplayObject newObj;
                            if (definition is Sprite)
                            {
                                ((Sprite)definition).graphics.CacheMeshes();
                                newObj = (DisplayObject)((DisplayObject)definition).Clone();
                            }
                            else if (definition is DisplayObject)
                            {
                                newObj = (DisplayObject)((DisplayObject)definition).Clone();
                            }
                            else
                            {
                                // For SWFs that place erroneous characters on the display list
                                // (e.g., fonts), create an empty Shape instead
                                newObj = new Shape();
                            }

                            this.depthObjectMap.Add(depth, newObj);
                            int addedAt = this.depthObjectMap.IndexOfKey(depth);
                            if (addedAt == (this.depthObjectMap.Count - 1))
                            {
                                // New object is higher that anything else so just add it
                                this.AddChild(newObj);
                            }
                            else
                            {
                                // Find the object that's right above newObj in the depth map
                                DisplayObject above = this.depthObjectMap.Values[addedAt + 1];

                                // Insert newObj at the location of that object
                                this.AddChildAt(newObj, this.GetChildIndex(above));
                            }
                        }
                        else
                        {
                            Log.Error(Subsystem.Playback,
                                "Unknown Character Id: {0}", entry.characterId);
                        }
                    }
                }

                // TODO bwmott 2013-09-18: Use frame set index to only do these when necessary.
                DisplayObject obj;
                if (this.depthObjectMap.TryGetValue(depth, out obj))
                {
                    if (entry.name != null)
                    {
                        obj.name = entry.name;
                    }
                    if (entry.matrix != null)
                    {
                        obj.transform.matrix.CopyFrom(entry.matrix);
                    }
                    if (entry.colorTransform != null)
                    {
                        obj.transform.colorTransform.CopyFrom(entry.colorTransform);
                    }
                }
            }

            // Now that all of the display objects are on the timeline, ensure that any active
            // clipping layers are added as masks to the corresponding display objects. This
            // is done only if the timeline contains masks.
            if (this.timeline.containsMasks)
            {
                // TODO bwmott 2014-06-11: This could be further optimized so that we only
                // process masks if this frame or the old frame contained masks.
                int clipDepth = 0;
                DisplayObject mask = null;
                foreach (var timelineEntry in newFrameEntries)
                {
                    int depth = timelineEntry.Key;
                    DisplayEntry entry = timelineEntry.Value;

                    DisplayObject obj;
                    if (this.depthObjectMap.TryGetValue(depth, out obj))
                    {
                        if (entry.clipDepth != 0)
                        {
                            clipDepth = entry.clipDepth;
                            mask = obj;
                        }
                        else if (depth <= clipDepth)
                        {
                            obj.mask = mask;
                        }
                        else
                        {
                            obj.mask = null;
                        }
                    }
                }
            }
        }

        private void QueueAS2ActionsAtFrame(int frameIndex)
        {
            List<TimelineAction> actions = timeline.GetActionsAt(frameIndex);
            for (int i = 0; i < actions.Count; ++i)
            {
                if (actions[i] is AS2Action)
                {
                    var as2 = (actions[i] as AS2Action).actions;
                    NativeWindow.windowActiveForScripts.as2Queue.Enqueue(as2, this);
                }
            }
        }
        
        private void ApplySoundActions(int frameIndex)
        {
            // Find the SoundActions
            List<TimelineAction> actions = timeline.GetActionsAt(frameIndex);
            for (int i = 0; i < actions.Count; ++i)
            {
                if (actions[i] is SoundAction)
                {
                    // Search dictionary for the Sound
                    SoundAction soundAction = (SoundAction)actions[i];
                    object obj = loaderInfo.context.dictionary[soundAction.soundId];
                    
                    // Check that the Sound was found
                    if (obj is Sound)
                    {
                        // Stop the sound or play it
                        if (soundAction.syncStop)
                        {
                            ((Sound)obj).Stop();
                        }
                        else
                        {
                            ((Sound)obj).Play(soundAction.syncNoMultiple);
                        }
                    }
                }
            }
        }
        
        private void ApplySoundStreamActions(int frameIndex)
        {
            // Check for previous SoundStreamHead
            if(streamSound != null)
            {
                bool hasAction = false;
                
                // Find SoundStreamActions
                List<TimelineAction> actions = timeline.GetActionsAt(frameIndex);
                foreach (TimelineAction action in actions)
                {
                    if (action is SoundStreamAction)
                    {
                        // Check for one SoundStreamBlock
                        if(hasAction)
                        {
                            Log.Error(Subsystem.Playback,
                                "Clip {0} has more than one SoundStreamBlock on frame {1}!",
                                this, frameIndex);
                            break;
                        }
                        
                        // Send audio data and play streamSound if not already playing
                        streamSound.Play(((SoundStreamAction) action).soundData);
                        hasAction = true;
                    }
                }
                
                // If no more SoundStreamBlocks
                if(!hasAction && streamSound.isPlaying)
                {
                    streamSound.Stop();
                }
            }
        }

        //--------------------------------------------------------------------------------
        
        internal class Timeline
        {
            private List<List<TimelineAction>> frameActions { get; set; }
            internal List<Dictionary<int, DisplayEntry>> timelineDisplayEntries { get; set; }
            internal bool containsMasks { get; set; }

            internal Timeline(int totalFrames = 0)
            {
                this.frameActions = new List<List<TimelineAction>>(totalFrames);
                this.timelineDisplayEntries = new List<Dictionary<int, DisplayEntry>>();
                this.containsMasks = false;

                for (int i = 0; i < totalFrames; ++i)
                {
                    this.frameActions.Add(new List<TimelineAction>(8));
                    this.timelineDisplayEntries.Add(new Dictionary<int, DisplayEntry>());
                }
            }

            internal List<TimelineAction> GetActionsAt(int index)
            {
                return this.frameActions[index];
            }

            internal void AddActionAt(TimelineAction action, int index)
            {
                this.frameActions[index].Add(action);
            }

            internal void BuildTimeLineDisplayEntries()
            {
                int totalFrames = this.frameActions.Count;

                for (int frame = 0; frame < totalFrames; ++frame)
                {
                    // Copy display entries from previous frame
                    if (frame > 0)
                    {
                        foreach (var entry in this.timelineDisplayEntries[frame - 1])
                        {
                            this.timelineDisplayEntries[frame].Add(entry.Key,
                                new DisplayEntry(entry.Value));
                        }
                    }

                    foreach (var action in this.frameActions[frame])
                    {
                        if (action is PlaceObjectAction)
                        {
                            PlaceObjectAction placeAction = (PlaceObjectAction)action;
                            int depth = placeAction.depth;
                            DisplayEntry entry = null;

                            if (placeAction.placeFlagMove)
                            {
                                // Either moving or replacing the character at the specified depth
                                entry = this.timelineDisplayEntries[frame][depth];

                                if (placeAction.characterId != 0)
                                {
                                    entry.characterId = placeAction.characterId;
                                    entry.characterIdFrame = frame;
                                }
                            }
                            else
                            {
                                entry = new DisplayEntry();
                                this.timelineDisplayEntries[frame].Add(depth, entry);

                                entry.characterId = placeAction.characterId;
                                entry.characterIdFrame = frame;

                                if (placeAction.matrix == null)
                                {
                                    entry.matrix = new Matrix();
                                    entry.matrixFrame = frame;
                                }

                                if (placeAction.colorTransform == null)
                                {
                                    entry.colorTransform = new ColorTransform();
                                    entry.colorTransformFrame = frame;
                                }
                            }

                            if (placeAction.name != null)
                            {
                                entry.name = placeAction.name;
                                entry.nameFrame = frame;
                            }

                            if (placeAction.matrix != null)
                            {
                                entry.matrix = placeAction.matrix;
                                entry.matrixFrame = frame;
                            }

                            if (placeAction.colorTransform != null)
                            {
                                entry.colorTransform = placeAction.colorTransform;
                                entry.colorTransformFrame = frame;
                            }

                            if (placeAction.clipDepth != 0)
                            {
                                entry.clipDepth = placeAction.clipDepth;
                                this.containsMasks = true;
                            }
                        }
                        else if (action is RemoveObjectAction)
                        {
                            RemoveObjectAction removeAction = (RemoveObjectAction)action;
                            this.timelineDisplayEntries[frame].Remove(removeAction.depth);
                        }
                    }
                }

                // TODO bwmott 2013-09-18: We can remove all of the DisplayActions from
                // the timeline since they are not needed any longer.
            }
        }

        internal abstract class TimelineAction
        {
            public TimelineAction()
            {
            }
        }

        internal abstract class TimelineDisplayAction : TimelineAction
        {
            public int depth { get; private set; }

            public TimelineDisplayAction(int depth)
            {
                this.depth = depth;
            }
        }

        internal class PlaceObjectAction : TimelineDisplayAction
        {
            public int characterId { get; private set; }
            public string name { get; private set; }
            public Matrix matrix { get; private set; }
            public ColorTransform colorTransform { get; private set; }
            public int clipDepth { get; private set; }
            public bool placeFlagMove { get; private set; }

            public PlaceObjectAction(int depth, int characterId, string name,
                Matrix matrix, ColorTransform colorTransform, int clipDepth,
                bool placeFlagMove = false)
                : base(depth)
            {
                this.characterId = characterId;
                this.name = name;
                this.matrix = matrix;
                this.colorTransform = colorTransform;
                this.clipDepth = clipDepth;
                this.placeFlagMove = placeFlagMove;
            }

            public override string ToString()
            {
                return string.Format("(depth={0}, characterId={1}, name={2}, " +
                    "matrix={3}, colorTransform={4}, clipDepth={5})", depth, characterId,
                    name, matrix, colorTransform, clipDepth);
            }
        }

        internal class RemoveObjectAction : TimelineDisplayAction
        {
            public RemoveObjectAction(int depth)
                : base(depth)
            {
            }

            public override string ToString ()
            {
                return string.Format("(depth={0})", depth);
            }
        }

        internal class AS2Action : TimelineAction
        {
            internal AS2 actions { get; private set; }

            public AS2Action(AS2 actions)
            {
                this.actions = actions;
            }
        }
        
        internal class SoundAction : TimelineAction
        {
            public int soundId { get; private set; }
            public bool syncStop { get; private set; }
            public bool syncNoMultiple { get; private set; }
            public int inPoint { get; private set; }
            public int outPoint { get; private set; }
            public int loopCount { get; private set; }
            
            public SoundAction(int soundId, bool syncStop, bool syncNoMultiple, int inPoint,
                int outPoint, int loopCount)
            {
                this.soundId = soundId;
                this.syncStop = syncStop;
                this.syncNoMultiple = syncNoMultiple;
                this.inPoint = inPoint;
                this.outPoint = outPoint;
                this.loopCount = loopCount;
            }
        }
        
        internal class SoundStreamAction : TimelineAction
        {
            public float[] soundData;
            
            public SoundStreamAction(float[] soundData)
            {
                this.soundData = soundData;
            }
        }

        //--------------------------------------------------------------------------------

        internal class DisplayEntry
        {
            public int characterId { get; set; }
            public int characterIdFrame { get; set; }
    
            public string name { get; set; }
            public int nameFrame { get; set; }
    
            public Matrix matrix { get; set; }
            public int matrixFrame { get; set; }
    
            public int clipDepth { get; set; }

            public ColorTransform colorTransform { get; set; }
            public int colorTransformFrame { get; set; }
    
            public DisplayEntry()
            {
                this.characterIdFrame = -1;
                this.nameFrame = -1;
                this.matrixFrame = -1;
                this.colorTransformFrame = -1;
            }
    
            public DisplayEntry(DisplayEntry obj)
            {
                this.characterId = obj.characterId;
                this.characterIdFrame = obj.characterIdFrame;
                this.name = obj.name;
                this.nameFrame = obj.nameFrame;
                this.matrix = obj.matrix;
                this.matrixFrame = obj.matrixFrame;
                this.clipDepth = obj.clipDepth;
                this.colorTransform = obj.colorTransform;
                this.colorTransformFrame = obj.colorTransformFrame;
            }
        }
    }
}
